package team.se.tms.plugin;

import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.RoutingStatementHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.scripting.defaults.DefaultParameterHandler;
import team.se.tms.dto.Page;
import team.se.tms.dto.ParameterPage;
import team.se.tms.util.ReflectUtil;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.Properties;

/**
 * 实现dataTable的服务器端，排序，分页，查询
 *
 * @author johnny
 * @date 2018/11/23
 */
@Intercepts({@Signature(method = "prepare", type = StatementHandler.class, args = {Connection.class,Integer.class}) })
public class DataTablePlugin implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //对于StatementHandler其实只有两个实现类，一个是RoutingStatementHandler，另一个是抽象类BaseStatementHandler，
        //BaseStatementHandler有三个子类，分别是SimpleStatementHandler，PreparedStatementHandler和CallableStatementHandler，
        //SimpleStatementHandler是用于处理Statement的，PreparedStatementHandler是处理PreparedStatement的，而CallableStatementHandler是
        //处理CallableStatement的。Mybatis在进行Sql语句处理的时候都是建立的RoutingStatementHandler，而在RoutingStatementHandler里面拥有一个
        //StatementHandler类型的delegate属性，RoutingStatementHandler会依据Statement的不同建立对应的BaseStatementHandler，即SimpleStatementHandler、
        //PreparedStatementHandler或CallableStatementHandler，在RoutingStatementHandler里面所有StatementHandler接口方法的实现都是调用的delegate对应的方法。
        //我们在PageInterceptor类上已经用@Signature标记了该Interceptor只拦截StatementHandler接口的prepare方法，又因为Mybatis只有在建立RoutingStatementHandler的时候
        //是通过Interceptor的plugin方法进行包裹的，所以我们这里拦截到的目标对象肯定是RoutingStatementHandler对象。
        RoutingStatementHandler handler = (RoutingStatementHandler) invocation.getTarget();
        //通过反射获取到当前RoutingStatementHandler对象的delegate属性
        StatementHandler delegate = (StatementHandler) ReflectUtil.getFieldValue(handler, "delegate");
        //获取到当前StatementHandler的 boundSql，这里不管是调用handler.getBoundSql()还是直接调用delegate.getBoundSql()结果是一样的，因为之前已经说过了
        //RoutingStatementHandler实现的所有StatementHandler接口方法里面都是调用的delegate对应的方法。


        BoundSql boundSql = delegate.getBoundSql();
        //拿到当前绑定Sql的参数对象，就是我们在调用对应的Mapper映射语句时所传入的参数对象
        Object obj = boundSql.getParameterObject();
        //传入的对象是Page或ParameterPage则认为要分页，否则直接放行
        Page<?> page = new Page<>();
        if(obj instanceof Page<?>){
            page = (Page<?>) obj;
        }else if(obj instanceof ParameterPage){
            page =((ParameterPage) obj).getPage();
        }else{
            return invocation.proceed();
        }
        if (page != null) {

            //通过反射获取StatementHandler delegate父类BaseStatementHandler的mappedStatement属性
            MappedStatement mappedStatement = (MappedStatement)ReflectUtil.getFieldValue(delegate, "mappedStatement");
            //拦截到的prepare方法参数是一个Connection对象
            Connection connection = (Connection)invocation.getArgs()[0];
            //获取当前要执行的Sql语句，也就是我们直接在Mapper映射语句中写的Sql语句
            String sql = boundSql.getSql();
            //给当前的page参数对象设置总记录数
            this.setTotalRecord(obj, mappedStatement, connection);
            //获取分页,排序,查询 Sql语句
            String pageSql = this.getSearchSql(page, sql);
            //利用反射设置当前BoundSql对应的sql属性为我们建立好的分页Sql语句
            ReflectUtil.setFieldValue(boundSql, "sql", pageSql);
        }
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        // 当目标类是StatementHandler类型时，才包装目标类，否者直接返回目标本身,减少目标被代理的次数
        if (target instanceof StatementHandler) {
            return Plugin.wrap(target, this);
        } else {
            return target;
        }
    }

    @Override
    public void setProperties(Properties properties) {


    }


    /**
     * 给当前的参数对象page设置总记录数
     *
     * @param obj Mapper映射语句对应的参数对象
     * @param mappedStatement 表示一个增删改查的详细信息,如<Select><Select/>
     * @param connection 当前的数据库连接
     * @create on 2018/11/23 by johnny
     */
    private void setTotalRecord(Object obj, MappedStatement mappedStatement, Connection connection) {
        //获取对应的BoundSql，这个BoundSql其实跟我们利用StatementHandler获取到的BoundSql是同一个对象。
        //delegate里面的boundSql也是通过mappedStatement.getBoundSql(paramObj)方法获取到的。
        BoundSql boundSql = mappedStatement.getBoundSql(obj);
        //获取到我们自己写在Mapper映射语句中对应的Sql语句
        String sql = boundSql.getSql();
        //通过查询Sql语句获取到对应的计算总记录数的sql语句
        String countSql = this.getCountSql(sql);
        //通过BoundSql获取对应的参数映射
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        //利用Configuration、查询记录数的Sql语句countSql、参数映射关系parameterMappings和参数对象page建立查询记录数对应的BoundSql对象。
        BoundSql countBoundSql = new BoundSql(mappedStatement.getConfiguration(), countSql, parameterMappings, obj);
        //通过mappedStatement、参数对象page和BoundSql对象countBoundSql建立一个用于设定参数的ParameterHandler对象
        ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, obj, countBoundSql);
        //通过connection建立一个countSql对应的PreparedStatement对象。
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            pstmt = connection.prepareStatement(countSql);
            //通过parameterHandler给PreparedStatement对象设置参数
            parameterHandler.setParameters(pstmt);
            //之后就是执行获取总记录数的Sql语句和获取结果了。
            rs = pstmt.executeQuery();
            if (rs.next()) {
                int totalRecord = rs.getInt(1);
                //给当前的参数page对象设置总记录数
                setTotalRecordToPage(obj,totalRecord);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            try {
                if (rs != null){
                    rs.close();
                }
                if (pstmt != null){
                    pstmt.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 获取原查询的总记录sql语句,注意 sql server中,select from (select ) as a
     *
     * @param sql  原sql语句
     * @return String 记数sql
     * @throws Exception 异常
     * @create on 2018/11/23 by johnny
     */
    private String getCountSql(String sql){
        return "select count(0) from (" + sql + ") as a";
    }
    /**
     * 封装sqlserver的分页,排序语句。
     *
     * @param sql 原sql语句
     * @param page mapper传入的Page参数
     * @return String 封装后的sql语句
     * @create on 2018/11/23 by johnny
     */
    private String  getPageSql(Page page,String sql){
        return sql+" order by " +page.getSortField()+" "+page.getOrderDir()+
                " offset " +page.getStart()+
                " rows fetch next "+page.getLength()+" rows only";
    }
    /**
     * 封装查询sql语句，并调用getPageSql获取分页sql，拼装完整sql语句
     *
     * @param page mapper传入的Page参数
     * @param sql 原sql语句
     * @return String 封装后的sql语句
     * @throws Exception 异常
     * @create on 2018/11/25 by johnny
     */
    private String getSearchSql(Page page,String sql){
        //搜索值，和拼接的表字段不为空
        if(null!=page.getConcatTableField() && !("".equals(page.getConcatTableField())) &&
                null!=page.getSearchValue() && !("".equals(page.getSearchValue()) ) ){
            //原sql语句中不包含where
            if(sql.contains("where")){
                String str[];
                str = sql.split("where");
                //防止sql语句中类似  where a=#{} left join 的情况
                sql = str[0]+" where "+page.getConcatTableField()+ " like '%"+page.getSearchValue()+"%' and "+str[1];
            }else{
                sql+=" where "+page.getConcatTableField()+ " like '%"+page.getSearchValue()+"%'";
            }

        }
        sql = getPageSql(page,sql);
        return sql;
    }
    /**
     * 因为不知道参数是Page 还是ParameterPage 所以抽取出来
     *
     * @param obj boundsql中的parameterObj
     * @param total 总记录数
     * @throws Exception 异常
     * @create on 2018/11/26 by johnny
     */
    private void setTotalRecordToPage(Object obj,int total){
        if(obj instanceof Page<?>){
            ((Page) obj).setTotalRecord(total);
        }else if(obj instanceof ParameterPage){
            ((ParameterPage) obj).getPage().setTotalRecord(total);
        }
    }

}
